﻿#region Disclaimer/Info
///////////////////////////////////////////////////////////////////////////////////////////////////
// MetaCollection
// 
// MetaCollection is a system to provide a standard structure, permissions and metadata service
// for content organization.
//
// This project is hosted at Google Code at http://code.google.com/p/metacollection/
//
// This project is licensed under the BSD license.  See the License.txt file for more information.
///////////////////////////////////////////////////////////////////////////////////////////////////
#endregion

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Globalization;
using System.IO;
using System.Linq;
using MetaCollection.Framework.Change;
using MetaCollection.Framework.Model;
using MetaCollection.Framework.Repository;
using MetaCollection.Framework.Security;
using System.Security.Principal;

namespace MetaCollection.Framework
{
	/// <summary>
	/// Item Context associated with a request. This class holds information used during the processing of a request.
	/// For reference this class is the 'work' in the 'unit of work' pattern being employed by the ItemApplication and
	/// IRequestHandlers.
	/// </summary>
	/// <remarks>This class must be thread safe.</remarks>
	public sealed class ItemContext : INotifyPropertyChanged
	{
		
		/// <summary>
		/// lock object used when changing properties.
		/// </summary>
		readonly object m_PropLock = new object();
		/// <summary>
		/// lock object for the response item collection.
		/// </summary>
		readonly object m_ItemLock = new object();
		/// <summary>
		/// value indicating if the path has been fully resolved.  Null indicates that no action has yet been taken.
		/// </summary>
		bool? m_RequestPathResolved;
		/// <summary>
		/// The requested item path as a URI
		/// </summary>
		Uri m_RequestPath;
		/// <summary>
		/// the index of the current segment of the path in the Uri <see cref="m_RequestPath"/>
		/// </summary>
		int m_CurrentPathSegment;
		/// <summary>
		/// holds the item identified by the <see cref="m_RequestPath"/> through the current path segment.
		/// </summary>
		Model.IItemInstance m_RequestedItem;
		/// <summary>
		/// a collection of the items returned by processing this request.
		/// </summary>
		IEnumerable<Model.IItemInstance> m_ResponseItems;
		/// <summary>
		/// a collection of form post data that is associated with this request.  This allows a request to interact
		/// with the resource provider of an item as if it were a regular webpage application.
		/// </summary>
		NameValueCollection m_PostData;
		/// <summary>
		/// For requests that apply to a specific version, this is that version number.
		/// </summary>
		int? m_SpecificVersion;
		/// <summary>
		/// unique identifier of this context.
		/// </summary>
		Guid m_ContextId;
		/// <summary>
		/// a set of options indicating which properties on an IItemInstance should be loaded from the database.
		/// </summary>
		LoadOptions m_LoadOptions;

		IItemRepository m_ItemRepository;
		IResourceRepository m_ResourceRepository;
		IModelRepository m_ModelRepository;
		IAuditRepository m_AuditRepository;
		/// <summary>
		/// Initializes a new instance of the <see cref="ItemContext"/> class.
		/// This initialization supports Deleting, getting an Item or an Item's Resource using a path to the item.
		/// </summary>
		/// <param name="user">The user.</param>
		/// <param name="requestType">Type of the request.</param>
		/// <param name="initialItem">The initial item is the root of the collection which the requests all start from.</param>
		/// <param name="requestPath">The requested item's path.</param>
		/// <param name="requestData">The request data which can be one of the following Predicate of IItemInstnace, Stream or IItemInstance.</param>
		internal ItemContext(MetaPrincipal user, IItemRepository itemRepository, IResourceRepository resourceRepository, IModelRepository modelRepository, IAuditRepository auditRepository, RequestTypes requestType, Model.IItemInstance initialItem, Uri requestPath, params object[] requestData)
		{
			if (initialItem == null)
				throw new ArgumentNullException("initialItem", "The initial item can not be null.");
			if (requestPath == null)
				throw new ArgumentException("Request Path can not be null.", "requestPath");
			if (!requestPath.Host.Equals(initialItem.Name, StringComparison.OrdinalIgnoreCase))
				throw new ArgumentException("initialItem Name does not match request path host.");
			if (!requestType.IsValid())
				throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, "Request Type \"{0}\" is not Valid", requestType), "requestType");
			if (itemRepository == null)
				throw new ArgumentNullException("itemRepository", "Item Repository Can not be Null.");
			if (resourceRepository == null)
				throw new ArgumentNullException("resourceRepository", "Resource Repository Can not be Null.");


			ProcessRequestData(requestType, requestData);

			m_ItemRepository = itemRepository;
			m_ModelRepository = modelRepository;
			m_ResourceRepository = resourceRepository;
			m_AuditRepository = auditRepository;

			m_ContextId = Guid.NewGuid();
			m_UserPerms = user;
			m_RequestType = requestType;
			m_RequestedItem = initialItem;
			m_RequestPath = requestPath;

			if (requestPath.Segments.Length <= 1)
			{
				m_RequestPathResolved = true;
			}
			else
			{
				m_CurrentPathSegment++; // advance past the initial '/' since this is our initialItem (root)
				m_RequestPathResolved = null; // We start with NULL since the path resolution event must determine this value.  In none is given then we know that resolution was not handled.
			}
			m_ResponseItems = new List<IItemInstance>();
		}

		/// <summary>
		/// Processes the request data parameter collection for the constructor.  Pulls values from 
		/// the collection as possible then verifies that the requested requesttype has the required
		/// parameters set.
		/// </summary>
		/// <param name="requestType">Type of the request.</param>
		/// <param name="requestData">The request data.</param>
		private void ProcessRequestData(RequestTypes requestType, object[] requestData)
		{
			if (requestData == null && !requestType.HasAnyType(RequestTypes.GetItem | RequestTypes.GetResource | RequestTypes.DeleteItem | RequestTypes.GetItemSpecification))
				throw new ArgumentException("Only Delete, Get Item (specification) or Get Resource request types are supported with this set of parameters.");
			else
			{
				m_LoadOptions = (LoadOptions)requestData.FirstOrDefault(r => r is LoadOptions, LoadOptions.ItemComplete);
				m_PostedItem = (IItemInstance)requestData.FirstOrDefault(r => r is IItemInstance, null);
				m_PostData = (NameValueCollection)requestData.FirstOrDefault(r => r is NameValueCollection, null);
				m_PostedResource = (Stream)requestData.FirstOrDefault(r => r is Stream, null);
				m_SpecificVersion = (int?)requestData.FirstOrDefault(r => r is Nullable<int>, null);
				if (requestType.HasType(RequestTypes.VersionSpecific) && !m_SpecificVersion.HasValue)
					throw new ArgumentException("Version specific requests require that the specific version in question be passed in as a parameter to the request.");
				if (requestType.HasAnyType(RequestTypes.SetItem) && m_PostedItem == null)
					throw new ArgumentException("Set Item Requires an IItemInstance parameter.");
				if (requestType.HasType(RequestTypes.SetResource) && m_PostedResource == null)
					throw new ArgumentException("A Set Resource Request requires a Stream parameter");
				if (requestType.HasType(RequestTypes.PostToItem) && m_PostData == null)
					throw new ArgumentException("The Post To Item request requires a set of Post Data in the form of a NameValueCollection as a parameter to the request.");
			}
		}
		/// <summary>
		/// Updates the path resolution by providing an item for CurrentPathSegment and advancing to
		/// the next path segment for resolution unless pathResolutionComplete has been set.  
		/// </summary>
		/// <remarks>
		/// If Path Resolution is not Set to Complete then the resolved Item name must match the current Path Segment.
		/// Hierarchical Paths
		/// -----------------------------------------
		/// The MetaCollection database uses a self referencing key to map items into a hierarchy.  The path segment value is stored
		/// with the versioned metadata for the item.  This presents us with several business rules related to versioned path segment
		/// data.  The v.1 system stored the path segment in the Title field of the metadata record along with the rest of the item’s
		/// metadata.  In v.2 the item’s metadata has been separated from the structure metadata.  This has been done to protect the
		/// structure information and fields from v.1 resource extensions which frequently remapped the standard metadata fields for
		/// other unintended uses.
		/// Regardless of the version the path segment field has a restricted set of allowed characters to match the requirements of
		/// RFC1738.  These characters are limited to upper and lower case letters, digits and a few extras 
		/// ("!" | "*" | "'" | "(" | ")" | "," | "$" | "-" | "_" | "." | "+").  While the specification allows the encoding of additional
		/// values through escape sequences the MetaCollection does not support this to protect URL clarity and security. In order
		/// to calculate the hierarchical path for an item
		/// 	1.	Select items having no parent (where ParentId is null) or use a previously specified root item.
		/// 	2.	Join items with versioned metadata where version is equal to the published version or failing that the most current
		/// 		version (presently it is assumed that the most current version is the one having the highest number however this is a bug
		/// 		(which maybe a required feature) and the most recently modified version should be selected).
		/// 	3.	Select the child item having a path segment value which matches the URL segment.  This match is not case sensitive.
		/// 	4.	For each subsequent path repeat steps one, two and three using the id of the found item as the ParentId when selecting
		/// 		child items
		/// 	5.	For the last path segment it is possible to have a pseudo-item named “default.mcpx”.  This item may not actually exist
		/// 	 	within the collection.  If this item is not found instead of returning a 404 error the parent item should be used instead.
		/// 	 	This behavior supports the ability for a parent item to be returned to the client as a folder which is used in the News
		/// 	 	component as a way to provide a category listing.
		/// </remarks>
		/// <param name="resolvedItem">The resolved item that was found for the CurrentPathSegment.</param>
		/// <param name="pathResolutionComplete">if set to <c>true</c> [path resolution complete].</param>
		/// <exception cref="ArgumentException">If arguments are not valid for the current item context.</exception>
		/// <exception cref="InvalidOperationException">Occurs if the an attempt to update path resolution is made on
		/// an item context where RequestPathResolved is true or if the resolvedItem name does not match the
		/// CurrentPathSegment and pathResolution is not complete</exception>
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1062:Validate arguments of public methods", MessageId = "0", Justification="The resolved item parameter has been validated before use.")]
		public void UpdatePathResolution(Model.IItemInstance resolvedItem, bool pathResolutionComplete)
		{
			if (resolvedItem == null)
				pathResolutionComplete = true; // no item was found (item null) so our path resolution attempt is complete.

			// lock here because we need to update several properties as a 'transaction' of sorts.
			lock (m_PropLock)
			{
				if (m_CurrentPathSegment >= m_RequestPath.Segments.Length)
					throw new InvalidOperationException("Can not update path resolution beyond the length of path segments to resolve.");
				if ((resolvedItem != null) && (m_RequestPathResolved.GetValueOrDefault(false)))
					throw new InvalidOperationException("Can not update path resolution when path has already been resolved.");
				// if the caller is not marking the resolution complete then demand that the work they are doing matches the path (either name or id).
				if (!pathResolutionComplete && 
						!(String.Equals(CurrentPathSegment, resolvedItem.Name, StringComparison.OrdinalIgnoreCase) ||
						String.Equals(CurrentPathSegment, resolvedItem.Id.ToString("D"), StringComparison.OrdinalIgnoreCase)))
					throw new InvalidOperationException("Resolved Item Name does not match Path Segment to be resolved.");
				
				m_RequestedItem = resolvedItem;
				m_CurrentPathSegment++;
				m_RequestPathResolved = pathResolutionComplete;
			}
			RaisePropertyChanged("RequestedItem");
			if (pathResolutionComplete)
				RaisePropertyChanged("RequestPathResolved");
			else
				RaisePropertyChanged("CurrentPathSegment");
		}

		/// <summary>
		/// Unique Identifier for this Context.
		/// </summary>
		public Guid ContextId { get { return m_ContextId; } }

		/// <summary>
		/// Gets the item repository for this Item Request Context.
		/// </summary>
		/// <value>The item repository.</value>
		public IItemRepository ItemRepository
		{
			get { return m_ItemRepository; }
		}

		/// <summary>
		/// Gets the audit repository for this Item Request Context.
		/// </summary>
		/// <value>The audit repository.</value>
		public IAuditRepository AuditRepository
		{
			get { return m_AuditRepository; }
		}

		/// <summary>
		/// Gets the model repository for this Item Request Context.
		/// </summary>
		/// <value>The model repository.</value>
		public IModelRepository ModelRepository
		{
			get { return m_ModelRepository; }
		}
		/// <summary>
		/// Gets the item posted with this request.
		/// </summary>
		/// <value>The request data.</value>
		public IItemInstance PostedItem
		{
			get
			{
				lock (m_PropLock)
					return m_PostedItem;
			}
		}
		private IItemInstance m_PostedItem;

		/// <summary>
		/// Gets the posted resource.
		/// </summary>
		/// <value>The posted resource.</value>
		public Stream PostedResource
		{
			get
			{
				lock (m_PropLock)
					return m_PostedResource;
			}
		}
		private Stream m_PostedResource;

		/// <summary>
		/// Gets the resource repository for this Item Request Context
		/// </summary>
		/// <value>The resource repository.</value>
		public IResourceRepository ResourceRepository
		{
			get { return m_ResourceRepository; }
		}

		/// <summary>
		/// Gets or sets the response Resource Stream for this request.
		/// </summary>
		/// <value>The response data.</value>
		public Stream ResponseResourceStream
		{
			get
			{
				lock (m_PropLock)
					return m_ResponseResourceStream;
			}
			set
			{
				lock (m_PropLock)
					m_ResponseResourceStream = value;
				RaisePropertyChanged("ResponseResourceStream");
			}
		}
		private Stream m_ResponseResourceStream;

		/// <summary>
		/// Gets or sets the item specification.
		/// </summary>
		/// <value>The item specification.</value>
		public IItemSpecification ItemSpecification
		{
			get
			{
				lock (m_PropLock)
					return m_ItemSpecification;
			}
			set
			{
				lock (m_PropLock)
					m_ItemSpecification = value;
				RaisePropertyChanged("ItemSpecification");
			}
		}
		private IItemSpecification m_ItemSpecification;

		/// <summary>
		/// A Readonly collection of response items.
		/// </summary>
		/// <value>The response data.</value>
		public IEnumerable<IItemInstance> ResponseItems
		{
			get
			{
				lock (m_ItemLock)
					return  m_ResponseItems;
			}
			set
			{
				lock (m_ItemLock)
					m_ResponseItems = value;
				RaisePropertyChanged("ResponseItems");
			}
		}

		/// <summary>
		/// Gets the post data.
		/// </summary>
		/// <value>The post data.</value>
		/// <exception cref="InvalidOperationException">thrown if the current request is not a PostToItem request.</exception>
		public NameValueCollection PostData
		{
			get
			{
				if (!RequestType.HasType(RequestTypes.PostToItem))
					throw new InvalidOperationException("Post Data is only available in a Post to Item Request.");
				return m_PostData;
			}
		}

		/// <summary>
		/// Adds an item to the Collection of Response Items.
		/// </summary>
		/// <param name="item">The item.</param>
		public void AddResponseItem(IItemInstance item)
		{
			lock (m_ItemLock)
			{
				if (m_ResponseItems.FirstOrDefault((i) => (i.Id == item.Id) && (i.Version == item.Version)) != null)
					throw new ArgumentException("Response Collection already contains this item.");
				m_ResponseItems = m_ResponseItems.Concat(new IItemInstance[] {item.ToStruct()}); // sanitize the item reference.
			}
			RaisePropertyChanged("ResponseItems");
		}

		/// <summary>
		/// Principal associated with this request.
		/// </summary>
		public MetaPrincipal User
		{
			get { return m_UserPerms; }
		}
		private MetaPrincipal m_UserPerms;

		/// <summary>
		/// Requested action for item
		/// </summary>
		public RequestTypes RequestType
		{
			get { return m_RequestType; }
		}
		private RequestTypes m_RequestType;

		/// <summary>
		/// Gets the specific version (this request is for).
		/// </summary>
		/// <value>The specific version this request applies to.</value>
		public int? SpecificVersion
		{
			get { return m_SpecificVersion; }
		}

		/// <summary>
		/// Indicates if this request is valid, invalid or unknown.
		/// </summary>
		/// <remarks>A null value indicates that the validitity of this request has not been determined.</remarks>
		/// <value>The is valid request.</value>
		public bool? IsValidRequest
		{
			get
			{
				lock (m_PropLock)
					return isValidRequest;
			}
			set
			{
				lock (m_PropLock)
					isValidRequest = value;
				RaisePropertyChanged("IsValidRequest");
			}
		}
		private bool? isValidRequest;

		/// <summary>
		/// An exception that may be associated with the processing of this request.
		/// </summary>
		// TODO : make this property into a stack with a push method to add new exceptions. Add a method to throw exceptions 
		// (RaiseAnyExceptions) that throws an exception with inner exceptions nested from the stack if exceptions are queued.
		public Exception ExecutionException
		{
			get
			{
				lock (m_PropLock)
					return executionException;
			}
			set
			{
				lock (m_PropLock)
					executionException = value;
				RaisePropertyChanged("ExecutionException");
			}
		}
		private Exception executionException;

		/// <summary>
		/// Gets or sets the Item that is associated with the current path
		/// </summary>
		/// <value>The item.</value>
		public Model.IItemInstance RequestedItem
		{
			get
			{
				lock (m_PropLock)
					return m_RequestedItem;
			}
			set
			{
				lock (m_PropLock)
					m_RequestedItem = value;
				RaisePropertyChanged("RequestedItem");
			}
		}

		private ItemChangeCollection m_Changes;
		public ItemChangeCollection Changes { 
			get 
			{
				lock (m_PropLock)
					return m_Changes;
			}
			set
			{
				lock (m_PropLock)
					m_Changes = value;
				RaisePropertyChanged("Changes");
			}
		}

		/// <summary>
		/// Gets or sets the load options.
		/// </summary>
		/// <value>The load options.</value>
		public LoadOptions LoadOptions
		{
			get
			{
				lock (m_PropLock)
					return m_LoadOptions;
			}
			set
			{
				lock (m_PropLock)
					m_LoadOptions = value;
				RaisePropertyChanged("LoadOptions");
			}
		}

		/// <summary>
		/// Path to a child of <see cref="ItemContext.Item"/> that may be pending resolution
		/// based on <see cref="ItemContext.RequestPathResolved"/>
		/// </summary>
		/// <remarks>
		/// This path takes the form of "initial-item-name/child-item-name/child-item-name" or "F6472894-7657-46bd-B955-81A5591A1B02"
		/// where the Guid is the Id of the item under RequestedItem (if not resolved).  This property is updated using the 
		/// <see cref="ItemContext.UpdatePathResolution"/> method.
		/// </remarks>
		public Uri RequestPath
		{
			get
			{
				lock (m_PropLock)
					return m_RequestPath;
			}
		}

		/// <summary>
		/// Gets the current path segment in the resolution process.
		/// </summary>
		/// <value>The current path segment.</value>
		public string CurrentPathSegment
		{
			get
			{
				lock (m_PropLock)
					return Uri.UnescapeDataString(m_RequestPath.Segments[m_CurrentPathSegment].Trim('/'));
			}
		}

		/// <summary>
		/// Gets the previous path segment in the resolution process.
		/// </summary>
		/// <value>The previous path segment.</value>
		/// <remarks>In case we navigate too far down, which is often the case for the trailing slash</remarks>
		public string PreviousPathSegment
		{
			get
			{
				lock(m_PropLock)
					return m_RequestPath.Segments[m_CurrentPathSegment-1];
			}
		}

		/// <summary>
		/// Gets a value indicating whether the CurrentPathSegment is the last path segment.
		/// </summary>
		/// <value>
		/// 	<c>true</c> if the CurrentPathSegment is last path segment; otherwise, <c>false</c>.
		/// </value>
		public bool IsLastPathSegment
		{
			get
			{
				lock (m_PropLock)
					return (m_CurrentPathSegment == (m_RequestPath.Segments.Length - 1));
			}
		}

		/// <summary>
		/// Indicates that the current path segment and ItemInstance are the fully resolved values and
		/// are ready for further processing
		/// </summary>
		public bool? RequestPathResolved
		{
			get
			{
				lock (m_PropLock)
					return m_RequestPathResolved;
			}
			set
			{
				lock (m_PropLock)
					m_RequestPathResolved = value;
				RaisePropertyChanged("RequestUriResolved");
			}
		}


		public event PropertyChangedEventHandler PropertyChanged;

		/// <summary>
		/// Raises the property changed event.
		/// </summary>
		/// <param name="name">The name.</param>
		private void RaisePropertyChanged(string name)
		{
			PropertyChangedEventHandler handler;

			lock (m_PropLock)
			{
				handler = PropertyChanged;				
			}

			if (handler != null)
			{
				handler(this, new PropertyChangedEventArgs(name));
			}
		}
				

		/// <summary>
		/// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
		/// </summary>
		/// <returns>
		/// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
		/// </returns>
		public override string ToString()
		{
			return String.Format(CultureInfo.CurrentCulture, "ItemContext([{0}],CurrentSegment-{1},Resolved-{2},Id-{3},Type-{4},Valid-{5})", RequestPath, CurrentPathSegment, RequestPathResolved, RequestedItem.Id, RequestType, IsValidRequest);
		}

	}

	internal static partial class Extensions
	{
		/// <summary>
		/// Uses the predicate to find a match in the source collection. If no match is found
		/// then the given default value is returned instead.
		/// </summary>
		/// <remarks>While very similar to the System.Linq version this one allows you to use an explicit
		/// value when none is found instead of the <c>default(TSource)</c> value</remarks>
		/// <typeparam name="TSource">The type of the source collection.</typeparam>
		/// <param name="source">The source collection.</param>
		/// <param name="predicate">The predicate.</param>
		/// <param name="defaultValue">value returned if the first element in the source is null</param>
		/// <returns>
		/// the value of the first element to satisfy the predicate or the defaultValue if none is found
		/// </returns>
		internal static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate, TSource defaultValue)
		{
			if ((source != null) && (source.Any(predicate)))
				return source.First(predicate);
			else
				return defaultValue;
		}
	}
}
